package repositories

import (
	"context"
	"errors"
	"fmt"
	"log"
	"strings"
	"time"

	"github.com/diggerhq/digger/opentaco/internal/domain"
	"github.com/diggerhq/digger/opentaco/internal/query/types"
	"github.com/diggerhq/digger/opentaco/internal/storage"
	"gorm.io/gorm"
)

const (
	queryByID          = "id = ?"
	errMsgOrgNotFound  = "failed to get organization: %w"
	errMsgUnitNotFound = "failed to find unit: %w"
)

// UnitRepository provides database-first unit management with blob storage backend
// This is the NEW architecture where database is source of truth
type UnitRepository struct {
	db          *gorm.DB
	blobStore   storage.UnitStore
	orgResolver domain.IdentifierResolver
}

// NewUnitRepository creates a repository with database as source of truth
func NewUnitRepository(db *gorm.DB, blobStore storage.UnitStore) *UnitRepository {
	return &UnitRepository{
		db:          db,
		blobStore:   blobStore,
		orgResolver: NewIdentifierResolver(db), // Use infrastructure layer implementation
	}
}

// Create creates a new unit with UUID and org-scoped storage
func (r *UnitRepository) Create(ctx context.Context, orgID, name string) (*storage.UnitMetadata, error) {
	// Get organization to validate and get org name
	var org types.Organization
	if err := r.db.WithContext(ctx).Where(queryByID, orgID).First(&org).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fmt.Errorf("organization not found: %s", orgID)
		}
		return nil, fmt.Errorf(errMsgOrgNotFound, err)
	}

	// Check if unit already exists
	var existing types.Unit
	err := r.db.WithContext(ctx).
		Where("org_id = ? AND name = ?", orgID, name).
		First(&existing).Error
	
	if err == nil {
		// Unit already exists - this is expected behavior, not an error
		return nil, storage.ErrAlreadyExists
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, fmt.Errorf("failed to check existing unit: %w", err)
	}

	// Create database record (UUID auto-generated by BeforeCreate hook)
	now := time.Now()
	unit := &types.Unit{
		OrgID:     orgID,
		Name:      name,
		Size:      0,
		UpdatedAt: now,
		Locked:    false,
	}

	if err := r.db.WithContext(ctx).Create(unit).Error; err != nil {
		// Check if this is a unique constraint violation
		// GORM doesn't have a specific error type, so we check the error string
		errMsg := err.Error()
		if strings.Contains(errMsg, "duplicate") || 
		   strings.Contains(errMsg, "unique constraint") ||
		   strings.Contains(errMsg, "UNIQUE constraint") ||
		   strings.Contains(errMsg, "idx_units_org_name") {
			return nil, storage.ErrAlreadyExists
		}
		return nil, fmt.Errorf("failed to create unit in database: %w", err)
	}

	// Construct UUID-based blob path: {org-uuid}/{unit-uuid}
	// This is immutable - unit renames won't affect S3 paths
	blobPath := fmt.Sprintf("%s/%s", org.ID, unit.ID)

	// Create blob in storage ASYNCHRONOUSLY for faster response
	// The unit is usable even if S3 isn't ready yet (first terraform apply will initialize it)
	go func() {
		// Use background context to avoid cancellation
		bgCtx := context.Background()
		_, err := r.blobStore.Create(bgCtx, blobPath)
		if err != nil {
			log.Printf("Async S3 creation failed for unit %s: %v - will be created on first use", unit.ID, err)
			// Note: Unit remains in DB even if S3 fails - terraform apply will create the state file
		}
	}()

	return &storage.UnitMetadata{
		ID:       unit.ID,      // UUID
		Name:     name,         // Short name
		OrgID:    orgID,        // Org UUID
		OrgName:  org.Name,    // Org short name (e.g., "acme")
		Size:     unit.Size,
		Updated:  unit.UpdatedAt,
		Locked:   unit.Locked,
	}, nil
}

// Get retrieves a unit by UUID
func (r *UnitRepository) Get(ctx context.Context, uuid string) (*storage.UnitMetadata, error) {
	var unit types.Unit
	err := r.db.WithContext(ctx).Preload("Tags").Where(queryByID, uuid).First(&unit).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, storage.ErrNotFound
		}
		return nil, fmt.Errorf("failed to get unit: %w", err)
	}

	// Get organization info
	var org types.Organization
	if err := r.db.WithContext(ctx).Where(queryByID, unit.OrgID).First(&org).Error; err != nil {
		return nil, fmt.Errorf(errMsgOrgNotFound, err)
	}

	// Construct UUID-based blob path: {org-uuid}/{unit-uuid}
	blobPath := fmt.Sprintf("%s/%s", org.ID, unit.ID)

	// Get blob metadata (size, lock info)
	blobMeta, err := r.blobStore.Get(ctx, blobPath)
	if err != nil && err != storage.ErrNotFound {
		return nil, fmt.Errorf("failed to get blob metadata: %w", err)
	}

	// Merge database and blob metadata
	meta := &storage.UnitMetadata{
		ID:       unit.ID,
		Name:     unit.Name,
		OrgID:    unit.OrgID,
		OrgName:  org.Name,
		Size:     unit.Size,
		Updated:  unit.UpdatedAt,
		Locked:   unit.Locked,
		LockID:   unit.LockID,
		
		// Include TFE workspace settings
		TFEAutoApply:        unit.TFEAutoApply,
		TFETerraformVersion: unit.TFETerraformVersion,
		TFEEngine:           unit.TFEEngine,
		TFEWorkingDirectory: unit.TFEWorkingDirectory,
		TFEExecutionMode:    unit.TFEExecutionMode,
	}

	// Use blob lock info if available
	if blobMeta != nil && blobMeta.LockInfo != nil {
		meta.LockInfo = blobMeta.LockInfo
		meta.Locked = true
	}

	return meta, nil
}

// List lists units with optional prefix filtering
func (r *UnitRepository) List(ctx context.Context, orgID, prefix string) ([]*storage.UnitMetadata, error) {
	var units []types.Unit
	query := r.db.WithContext(ctx).Where("org_id = ?", orgID)
	
	if prefix != "" {
		query = query.Where("name LIKE ?", prefix+"%")
	}
	
	if err := query.Find(&units).Error; err != nil {
		return nil, fmt.Errorf("failed to list units: %w", err)
	}

	// Try to get org name from context to avoid DB query (optimization)
	orgName := ""
	if orgCtx, ok := domain.OrgFromContext(ctx); ok && orgCtx.OrgName != "" {
		orgName = orgCtx.OrgName
	} else {
		// Fallback: query database for org info
		var org types.Organization
		if err := r.db.WithContext(ctx).Where(queryByID, orgID).First(&org).Error; err != nil {
			return nil, fmt.Errorf(errMsgOrgNotFound, err)
		}
		orgName = org.Name
	}

	result := make([]*storage.UnitMetadata, len(units))
	for i, unit := range units {
		result[i] = &storage.UnitMetadata{
			ID:       unit.ID,
			Name:     unit.Name,
			OrgID:    unit.OrgID,
			OrgName:  orgName,
			Size:     unit.Size,
			Updated:  unit.UpdatedAt,
			Locked:   unit.Locked,
		}
	}

	return result, nil
}

// Delete deletes a unit by UUID
func (r *UnitRepository) Delete(ctx context.Context, uuid string) error {
	var unit types.Unit
	err := r.db.WithContext(ctx).Where(queryByID, uuid).First(&unit).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return storage.ErrNotFound
		}
		return fmt.Errorf(errMsgUnitNotFound, err)
	}

	// Get organization info
	var org types.Organization
	if err := r.db.WithContext(ctx).Where(queryByID, unit.OrgID).First(&org).Error; err != nil {
		return fmt.Errorf(errMsgOrgNotFound, err)
	}

	// Construct UUID-based blob path: {org-uuid}/{unit-uuid}
	blobPath := fmt.Sprintf("%s/%s", org.ID, unit.ID)

	// Delete from blob storage first
	if err := r.blobStore.Delete(ctx, blobPath); err != nil && err != storage.ErrNotFound {
		return fmt.Errorf("failed to delete blob: %w", err)
	}

	// Delete from database
	if err := r.db.WithContext(ctx).Delete(&unit).Error; err != nil {
		return fmt.Errorf("failed to delete unit from database: %w", err)
	}

	log.Printf("Deleted unit: UUID=%s, Org=%s (%s), Name=%s, BlobPath=%s", 
		uuid, org.Name, org.ID, unit.Name, blobPath)
	return nil
}

// Download downloads unit data by UUID
func (r *UnitRepository) Download(ctx context.Context, uuid string) ([]byte, error) {
	var unit types.Unit
	err := r.db.WithContext(ctx).Where(queryByID, uuid).First(&unit).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, storage.ErrNotFound
		}
		return nil, fmt.Errorf(errMsgUnitNotFound, err)
	}

	// Get organization info
	var org types.Organization
	if err := r.db.WithContext(ctx).Where(queryByID, unit.OrgID).First(&org).Error; err != nil {
		return nil, fmt.Errorf(errMsgOrgNotFound, err)
	}

	// Construct UUID-based blob path: {org-uuid}/{unit-uuid}
	blobPath := fmt.Sprintf("%s/%s", org.ID, unit.ID)

	// Download from blob storage
	return r.blobStore.Download(ctx, blobPath)
}

// GetLock retrieves lock information for a unit by UUID
func (r *UnitRepository) GetLock(ctx context.Context, uuid string) (*storage.LockInfo, error) {
	var unit types.Unit
	err := r.db.WithContext(ctx).Where(queryByID, uuid).First(&unit).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, storage.ErrNotFound
		}
		return nil, fmt.Errorf(errMsgUnitNotFound, err)
	}

	// If not locked, return nil
	if !unit.Locked {
		return nil, nil
	}

	lockInfo := &storage.LockInfo{
		ID:  unit.LockID,
		Who: unit.LockWho,
	}
	if unit.LockCreated != nil {
		lockInfo.Created = *unit.LockCreated
	}
	return lockInfo, nil
}

// Upload uploads unit data by UUID
func (r *UnitRepository) Upload(ctx context.Context, uuid string, data []byte, lockID string) error {
	var unit types.Unit
	err := r.db.WithContext(ctx).Where(queryByID, uuid).First(&unit).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return storage.ErrNotFound
		}
		return fmt.Errorf(errMsgUnitNotFound, err)
	}

	// Get organization info
	var org types.Organization
	if err := r.db.WithContext(ctx).Where(queryByID, unit.OrgID).First(&org).Error; err != nil {
		return fmt.Errorf(errMsgOrgNotFound, err)
	}

	// Construct UUID-based blob path: {org-uuid}/{unit-uuid}
	blobPath := fmt.Sprintf("%s/%s", org.ID, unit.ID)

	// Upload to blob storage
	if err := r.blobStore.Upload(ctx, blobPath, data, lockID); err != nil {
		return err
	}

	// Update database metadata
	if err := r.db.WithContext(ctx).Model(&unit).Updates(map[string]interface{}{
		"size":       int64(len(data)),
		"updated_at": time.Now(),
	}).Error; err != nil {
		return fmt.Errorf("failed to update unit metadata: %w", err)
	}

	return nil
}

// Lock locks a unit by UUID
func (r *UnitRepository) Lock(ctx context.Context, uuid string, lockInfo *storage.LockInfo) error {
	var unit types.Unit
	err := r.db.WithContext(ctx).Where(queryByID, uuid).First(&unit).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return storage.ErrNotFound
		}
		return fmt.Errorf(errMsgUnitNotFound, err)
	}

	// Get organization info
	var org types.Organization
	if err := r.db.WithContext(ctx).Where(queryByID, unit.OrgID).First(&org).Error; err != nil {
		return fmt.Errorf(errMsgOrgNotFound, err)
	}

	// Construct UUID-based blob path: {org-uuid}/{unit-uuid}
	blobPath := fmt.Sprintf("%s/%s", org.ID, unit.ID)

	// Lock in blob storage
	if err := r.blobStore.Lock(ctx, blobPath, lockInfo); err != nil {
		return err
	}

	// Update database
	if err := r.db.WithContext(ctx).Model(&unit).Updates(map[string]interface{}{
		"locked":       true,
		"lock_id":      lockInfo.ID,
		"lock_who":     lockInfo.Who,
		"lock_created": lockInfo.Created,
	}).Error; err != nil {
		return fmt.Errorf("failed to update lock in database: %w", err)
	}

	return nil
}

// Unlock unlocks a unit by UUID
func (r *UnitRepository) Unlock(ctx context.Context, uuid string, lockID string) error {
	var unit types.Unit
	err := r.db.WithContext(ctx).Where(queryByID, uuid).First(&unit).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return storage.ErrNotFound
		}
		return fmt.Errorf(errMsgUnitNotFound, err)
	}

	// Get organization info
	var org types.Organization
	if err := r.db.WithContext(ctx).Where(queryByID, unit.OrgID).First(&org).Error; err != nil {
		return fmt.Errorf(errMsgOrgNotFound, err)
	}

	// Construct UUID-based blob path: {org-uuid}/{unit-uuid}
	blobPath := fmt.Sprintf("%s/%s", org.ID, unit.ID)

	// Unlock in blob storage
	if err := r.blobStore.Unlock(ctx, blobPath, lockID); err != nil {
		return err
	}

	// Update database
	if err := r.db.WithContext(ctx).Model(&unit).Updates(map[string]interface{}{
		"locked":       false,
		"lock_id":      "",
		"lock_who":     "",
		"lock_created": nil,
	}).Error; err != nil {
		return fmt.Errorf("failed to update unlock in database: %w", err)
	}

	return nil
}

// ListVersions lists versions for a unit by UUID
func (r *UnitRepository) ListVersions(ctx context.Context, uuid string) ([]*storage.VersionInfo, error) {
	var unit types.Unit
	err := r.db.WithContext(ctx).Where(queryByID, uuid).First(&unit).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, storage.ErrNotFound
		}
		return nil, fmt.Errorf(errMsgUnitNotFound, err)
	}

	// Get organization info
	var org types.Organization
	if err := r.db.WithContext(ctx).Where(queryByID, unit.OrgID).First(&org).Error; err != nil {
		return nil, fmt.Errorf(errMsgOrgNotFound, err)
	}

	// Construct UUID-based blob path: {org-uuid}/{unit-uuid}
	blobPath := fmt.Sprintf("%s/%s", org.ID, unit.ID)

	return r.blobStore.ListVersions(ctx, blobPath)
}

// RestoreVersion restores a version for a unit by UUID
func (r *UnitRepository) RestoreVersion(ctx context.Context, uuid string, versionTimestamp time.Time, lockID string) error {
	var unit types.Unit
	err := r.db.WithContext(ctx).Where(queryByID, uuid).First(&unit).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return storage.ErrNotFound
		}
		return fmt.Errorf(errMsgUnitNotFound, err)
	}

	// Get organization info
	var org types.Organization
	if err := r.db.WithContext(ctx).Where(queryByID, unit.OrgID).First(&org).Error; err != nil {
		return fmt.Errorf(errMsgOrgNotFound, err)
	}

	// Construct UUID-based blob path: {org-uuid}/{unit-uuid}
	blobPath := fmt.Sprintf("%s/%s", org.ID, unit.ID)

	return r.blobStore.RestoreVersion(ctx, blobPath, versionTimestamp, lockID)
}

// ResolveIdentifier resolves a unit identifier (UUID, name, or absolute name) to UUID
func (r *UnitRepository) ResolveIdentifier(ctx context.Context, identifier, orgID string) (string, error) {
	return r.orgResolver.ResolveUnit(ctx, identifier, orgID)
}
